﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;

namespace Microsoft.PKS
{
	/// <summary>
	/// Represents a media file object
	/// </summary>
	public class MediaInfo
	{
		/// <summary>
		/// The resolution of the media's largest video stream in pixels.
		/// </summary>
		public Size Resolution { get { return resolution; } }
		private Size resolution = Size.Empty;

		/// <summary>
		/// The duration of the media's longest stream in seconds.
		/// </summary>
		public double Duration { get { return duration; } }
		private double duration = 0;

		/// <summary>
		/// The size of the media file in bytes.
		/// </summary>
		public long FileSize { get { return fileSize; } }
		private long fileSize = 0;

		/// <summary>
		/// True if the media file has a video stream, False if it does not.
		/// </summary>
		public bool HasVideo { get { return hasVideo; } }
		private bool hasVideo = false;

		/// <summary>
		/// True if the media file exists, False if it does not.
		/// </summary>
		public bool Exists { get { return exists; } }
		private bool exists = false;

		/// <summary>
		/// True if the file is a valid media file, False if it is not.
		/// </summary>
		public bool ValidMediaFile { get { return validMediaFile; } }
		private bool validMediaFile = false;

		/// <summary>
		/// The path and filename of the media file on disk.
		/// </summary>
		public string FileName { get { return fileName; } }
		private string fileName = string.Empty;

		/// <summary>
		/// Private variable, True if running on 32 bit architecture, false otherwise
		/// </summary>
		private static bool isRunning32Bit = (IntPtr.Size == 4);

		/// <summary>
		/// Returns a string representing the object.
		/// </summary>
		/// <returns>Returns a string representing the object.</returns>
		public override string ToString()
		{
			StringBuilder sb = new StringBuilder();
			sb.Append("File Name: ");
			sb.AppendLine(FileName);

			sb.Append("Resolution: ");
			sb.Append(Resolution.Width.ToString());
			sb.Append("x");
			sb.AppendLine(Resolution.Height.ToString());

			sb.Append("Duration: ");
			sb.Append(Duration.ToString());
			sb.AppendLine(" second(s)");

			sb.Append("Exists: ");
			sb.AppendLine(Exists.ToString());

			sb.Append("File Size: ");
			sb.Append(FileSize.ToString());
			sb.AppendLine(" byte(s)");

			sb.Append("Valid Media File: ");
			sb.AppendLine(ValidMediaFile.ToString());

			sb.Append("Has Video: ");
			sb.AppendLine(HasVideo.ToString());

			/*sb.Append("System Architecture: ");
			if (isRunning32Bit)
			{ sb.AppendLine("32 bit"); }
			else
			{ sb.AppendLine("Not 32 bit"); }*/

			return sb.ToString();
		}

		/// <summary>
		/// Creates a new MediaInfo object.
		/// </summary>
		/// <param name="fileName">The path and filename of the media file to inspect.</param>
		public MediaInfo(string fileName)
		{
			if (string.IsNullOrEmpty(fileName)) { throw new ArgumentNullException("fileName"); }
			//if (!File.Exists(fileName)) { throw new ArgumentException("fileName", "File does not exist: \"" + fileName + "\""); }
			
			this.fileName = fileName;
			
			//Inspect the media file
			RefreshInfo();
		}

		// Set to Marshal.SizeOf(new WinApiInteropStructs.VideoInfoHeader()) when RefreshInfo is called for the first time.
		private static int VideoHeaderSize = 0;

		// Used by RefreshInfo to determine if the file contains a video stream.
		private static Guid MediaTypeVideo = new Guid(0x73646976, 0x0000, 0x0010, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71);
		
		/// <summary>
		/// Reinspects the media file (you would call this if the file had been updated since you created the object).
		/// </summary>
		public void RefreshInfo()
		{
			if (VideoHeaderSize == 0)
			{ VideoHeaderSize = Marshal.SizeOf(new WinApiInteropStructs.VideoInfoHeader()); }

			//Reset Properties
			validMediaFile = false;
			hasVideo = false;
			duration = 0;
			resolution = Size.Empty;
			fileSize = 0;
			exists = false;

			bool attemptToReadVideoStream = isRunning32Bit;

			try
			{
				exists = File.Exists(fileName);
				if (!exists) { return; }

				FileInfo fi = new FileInfo(FileName);
				fileSize = fi.Length;

				if (!string.IsNullOrEmpty(fi.Extension))
				{
					string ext = fi.Extension.ToLower();
					if (ext.Length > 1) { ext = ext.Substring(1); }

					if (ext == "wma" || ext == "wav" || ext == "mp2" || ext == "mp3")
					{
						attemptToReadVideoStream = false;
					}
				}

				//Open the media
				DexterLib.MediaDetClass det = new DexterLib.MediaDetClass();
				det.Filename = FileName;

				int StreamCount = det.OutputStreams;

				//Interrogate the media file
				for (int i = 0; i < StreamCount; i++)
				{
					//Switch the current stream to our index counter
					det.CurrentStream = i;

					//Is the current stream longer than any other stream?
					if (det.StreamLength > duration) { duration = det.StreamLength; }

					//Is the current stream a video stream?
					if (det.StreamMediaType.majortype == MediaTypeVideo)
					{
						try
						{
							if (attemptToReadVideoStream)
							{
								//As far as I can tell, pbFormat is a pointer to a structure, and cbFormat is the size of that structure.
								if (det.StreamMediaType.cbFormat >= VideoHeaderSize)
								{
									IntPtr ip = det.StreamMediaType.pbFormat;

									if (ip != IntPtr.Zero)
									{
										WinApiInteropStructs.VideoInfoHeader videoInfo = (WinApiInteropStructs.VideoInfoHeader)Marshal.PtrToStructure(ip, typeof(WinApiInteropStructs.VideoInfoHeader));

										//First video stream in media file
										if (resolution == Size.Empty)
										{
											resolution = new Size(videoInfo.bmiHeader.biWidth, videoInfo.bmiHeader.biHeight);
										}
										else
										{
											//The media file might contain more than one video stream
											int w = videoInfo.bmiHeader.biWidth;
											int h = videoInfo.bmiHeader.biHeight;
											if (w > resolution.Width) { resolution = new Size(w, resolution.Height); }
											if (h > resolution.Height) { resolution = new Size(resolution.Width, h); }
										}
									}
								}
							}
							hasVideo = true;
						}
						catch (Exception ex)
						{
							Trace.WriteLine("Could not get the video stream properties, reporting media as not containing a video stream, this could possibly be a codec issue.\n\tException: " + ex.ToString());
						}
					}
				}

				validMediaFile = true;
			}
			catch
			{
				validMediaFile = false;
				hasVideo = false;
				duration = 0;
				resolution = Size.Empty;
			}
		}

		/// <summary>
		/// Returns a thumbnail from the position provided.
		/// </summary>
		/// <param name="position">The position in seconds into the video the thumbnail should be taken.</param>
		/// <param name="targetSize">The target thumbnail size.</param>
		/// <returns>A bitmap thumbnail.</returns>
		public Bitmap RetrieveThumbnail(double position, Size targetSize)
		{
			return GfxUtils.HighQualityBitmapResizer.ResizeBitmap(RetrieveThumbnail(position), targetSize);
		}

		/// <summary>
		/// Returns a thumbnail from the position provided.
		/// </summary>
		/// <param name="position">The position in seconds into the video the thumbnail should be taken.</param>
		/// <param name="targetSize">The target thumbnail size.</param>
		/// <param name="allowDistortion">Should the resize routine allow distortion?</param>
		/// <returns>A bitmap thumbnail.</returns>
		public Bitmap RetrieveThumbnail(double position, Size targetSize, bool allowDistortion)
		{
			return GfxUtils.HighQualityBitmapResizer.ResizeBitmap(RetrieveThumbnail(position), targetSize, allowDistortion);
		}

		/// <summary>
		/// Returns a thumbnail from the position provided.
		/// </summary>
		/// <param name="position">The position in seconds into the video the thumbnail should be taken.</param>
		/// <param name="targetSize">The target thumbnail size.</param>
		/// <param name="allowDistortion">Should the resize routine allow distortion?</param>
		/// <param name="backgroundColor">The color of the bars on to put on the sides of the thumbnail when preventing distortion.</param>
		/// <returns>A bitmap thumbnail.</returns>
		public Bitmap RetrieveThumbnail(double position, Size targetSize, bool allowDistortion, Color backgroundColor)
		{
			return GfxUtils.HighQualityBitmapResizer.ResizeBitmap(RetrieveThumbnail(position), targetSize, allowDistortion, backgroundColor);
		}


		/// <summary>
		/// Saves a thumbnail of the media file to disk.
		/// 
		/// 32 Bit: All parameters valid and used, if Size.Empty is sent as target size, the resolution of the video is used.
		/// 64 Bit: allowDistortion and background color are ignored, the image will always allow distortion, if Size.Empty is
		///		passed in as the size, it will be changed to 320 x 240.
		/// 
		/// </summary>
		/// <param name="outputFileName">The place to save the thumbnail.</param>
		/// <param name="position">The position in seconds into the video the thumbnail should be taken.</param>
		/// <param name="targetSize">The target thumbnail size.</param>
		/// <param name="allowDistortion">Should the resize routine allow distortion?</param>
		/// <param name="backgroundColor">The color of the bars on to put on the sides of the thumbnail when preventing distortion.</param>
		public void SaveThumbnail(string outputFileName, double position, Size targetSize, bool allowDistortion, Color backgroundColor)
		{
			if (string.IsNullOrEmpty(outputFileName)) { throw new ArgumentNullException("outputFileName"); }
			if (isRunning32Bit)
			{
				try
				{
					Bitmap bmp = RetrieveThumbnail(position, targetSize, allowDistortion, backgroundColor);
					if (bmp != null)
					{
						bmp.Save(outputFileName, ImageFormat.Bmp);
					}
					return;
				}
				catch{}
			}

			RefreshInfo();
			if (targetSize == Size.Empty) { targetSize = this.Resolution; }
			if (targetSize == Size.Empty) { targetSize = new Size(320, 240); }

			if (!exists) { throw new Exception("The file does not exist."); }
			if (!validMediaFile) { throw new Exception("The file is not a valid media file."); }
			if (!hasVideo) { throw new Exception("The media file does not contain a usable video stream!"); }

			if (position != 0 || duration != 0)
			{
				if (position < 0 || position >= Duration) { throw new ArgumentException("position", "Position must be more than or equal to zero and less than Duration"); }
			}

			DexterLib.MediaDetClass det = new DexterLib.MediaDetClass();
			det.Filename = FileName;
			det.WriteBitmapBits(position, targetSize.Width, targetSize.Height, outputFileName);
		}



		/// <summary>
		/// Returns a thumbnail from the position provided.
		/// </summary>
		/// <param name="position">The position in seconds into the video the thumbnail should be taken.</param>
		/// <returns>A bitmap thumbnail.</returns>
		public Bitmap RetrieveThumbnail(double position)
		{
			if (!isRunning32Bit) { throw new Exception("This platform cannot retrieve Thumbnails."); }
			RefreshInfo();
			if (!exists) { throw new Exception("The file does not exist."); }
			if (!validMediaFile) { throw new Exception("The file is not a valid media file."); }
			if ((!hasVideo) || ((Resolution.Width < 1) || (Resolution.Height < 1))) { throw new Exception("The media file does not contain a usable video stream!"); }

			if (position != 0 || duration != 0)
			{
				if (position < 0 || position >= Duration) { throw new ArgumentException("position", "Position must be more than or equal to zero and less than Duration"); }
			}

			int rowLength = Resolution.Width * 3;
			int bufferSize = (rowLength * Resolution.Height) + VideoHeaderSize;

			byte[] buffer = new byte[bufferSize];

			//Open the movie
			DexterLib.MediaDetClass det = new DexterLib.MediaDetClass();
			det.Filename = FileName;

			det.GetBitmapBits(position, ref bufferSize, ref buffer[0], Resolution.Width, Resolution.Height);

			//Copy bytes into bitmap
			Bitmap bmp = new Bitmap(Resolution.Width, Resolution.Height, PixelFormat.Format24bppRgb);
			BitmapData bd = bmp.LockBits(new Rectangle(Point.Empty, Resolution), ImageLockMode.WriteOnly, PixelFormat.Format24bppRgb);

			bool quickCopy = false;
			if (bd.Stride == rowLength)
			{
				Marshal.Copy(buffer, VideoHeaderSize, bd.Scan0, rowLength * Resolution.Height);
				quickCopy = true;
				
			}
			else
			{
				//Depending on the created image, stride might be padded to be longer than row length,
				// we flip the image manually during this step because it's convienient and saves us
				// the hassle of making a seperate system call.
				
				IntPtr row = bd.Scan0;
				int startIndex = VideoHeaderSize + ((Resolution.Height - 1) * rowLength);
				for (int y = 0; y < Resolution.Height; y++)
				{
					Marshal.Copy(buffer, startIndex, row, rowLength);

					row = (IntPtr)((int)row + bd.Stride);
					startIndex -= rowLength;
				}
			}

			bmp.UnlockBits(bd);
			
			//The image that comes back via the quick copy is vertically flipped
			if (quickCopy)
			{ bmp.RotateFlip(RotateFlipType.RotateNoneFlipY); }
			
			return bmp;
		}

		private static class WinApiInteropStructs
		{
			[StructLayout(LayoutKind.Sequential)]
			public struct Rect
			{
				Int32 left;
				Int32 top;
				Int32 right;
				Int32 bottom;
			}

			/// <summary>
			/// The VideoInfoHeader structure describes the bitmap and color information for a video image
			/// </summary>
			[StructLayout(LayoutKind.Sequential)]
			public struct VideoInfoHeader
			{
				/// <summary>RECT structure that specifies the source video window. This structure can be a clipping rectangle, to select a portion of the source video stream.</summary>
				public Rect rcSource;
				/// <summary>RECT structure that specifies the destination video window.</summary>
				public Rect rcTarget;
				/// <summary>Approximate data rate of the video stream, in bits per second</summary>
				public UInt32 dwBitRate;
				/// <summary>Data error rate, in bit errors per second</summary>
				public UInt32 dwBitErrorRate;
				/// <summary>The desired average display time of the video frames, in 100-nanosecond units. The actual time per frame may be longer. See Remarks.</summary>
				public Int64 AvgTimePerFrame;
				/// <summary>BITMAPINFOHEADER structure that contains color and dimension information for the video image bitmap. If the format block contains a color table or color masks, they immediately follow the bmiHeader member. You can get the first color entry by casting the address of member to a BITMAPINFO pointer</summary>
				public BitmapInfoHeader bmiHeader;
			}
			/// <summary>
			/// The BitmapInfoHeader structure contains information about the dimensions and color format of a device-independent bitmap (DIB). 
			/// SEE MSDN
			/// </summary>
			[StructLayout(LayoutKind.Sequential)]
			public struct BitmapInfoHeader
			{
				/// <summary>Specifies the number of bytes required by the structure. This value does not include the size of the color table or the size of the color masks, if they are appended to the end of structure. See Remarks.</summary>
				public UInt32 biSize;
				/// <summary>Specifies the width of the bitmap, in pixels. For information about calculating the stride of the bitmap, see Remarks.</summary>
				public Int32 biWidth;
				/// <summary>Specifies the height of the bitmap, in pixels. SEE MSDN</summary>
				public Int32 biHeight;
				/// <summary>Specifies the number of planes for the target device. This value must be set to 1</summary>
				public UInt16 biPlanes;
				/// <summary>Specifies the number of bits per pixel (bpp).  For uncompressed formats, this value gives to the average number of bits per pixel. For compressed formats, this value gives the implied bit depth of the uncompressed image, after the image has been decoded.</summary>
				public UInt16 biBitCount;
				/// <summary>For compressed video and YUV formats, this member is a FOURCC code, specified as a DWORD in little-endian order. For example, YUYV video has the FOURCC 'VYUY' or 0x56595559. SEE MSDN</summary>
				public UInt32 biCompression;
				/// <summary>Specifies the size, in bytes, of the image. This can be set to 0 for uncompressed RGB bitmaps</summary>
				public UInt32 biSizeImage;
				/// <summary>Specifies the horizontal resolution, in pixels per meter, of the target device for the bitmap</summary>
				public Int32 biXPelsPerMeter;
				/// <summary>Specifies the vertical resolution, in pixels per meter, of the target device for the bitmap</summary>
				public Int32 biYPelsPerMeter;
				/// <summary>Specifies the number of color indices in the color table that are actually used by the bitmap. See Remarks for more information.</summary>
				public UInt32 biClrUsed;
				/// <summary>Specifies the number of color indices that are considered important for displaying the bitmap. If this value is zero, all colors are important</summary>
				public UInt32 biClrImportant;
			}

		}
	}
}

